package chooongg.base.utils

import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import chooongg.base.manager.AppManager.getApp
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.*
import java.lang.Character.isSpace
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

const val V = Log.VERBOSE
const val D = Log.DEBUG
const val I = Log.INFO
const val W = Log.WARN
const val E = Log.ERROR
const val A = Log.ASSERT

@IntDef(V, D, I, W, E, A)
annotation class TYPE

private val logT = charArrayOf('V', 'D', 'I', 'W', 'E', 'A')

private const val FILE = 0x10
private const val JSON = 0x20
private const val XML = 0x30

private val FILE_SEP = System.getProperty("file.separator")
private val LINE_SEP = System.getProperty("line.separator")
private const val TOP_CORNER = "┌"
private const val MIDDLE_CORNER = "├"
private const val LEFT_BORDER = "│ "
private const val BOTTOM_CORNER = "└"
private const val SIDE_DIVIDER = "────────────────────────────────────────────────────────"
private const val MIDDLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"
private const val TOP_BORDER = TOP_CORNER + SIDE_DIVIDER + SIDE_DIVIDER + SIDE_DIVIDER
private const val MIDDLE_BORDER = MIDDLE_CORNER + MIDDLE_DIVIDER + MIDDLE_DIVIDER + MIDDLE_DIVIDER
private const val BOTTOM_BORDER = BOTTOM_CORNER + SIDE_DIVIDER + SIDE_DIVIDER + SIDE_DIVIDER
private const val MAX_LEN = 3000
@SuppressLint("SimpleDateFormat")
private val FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS ")
private const val NOTHING = "log nothing"
private const val NULL = "null"
private const val ARGS = "args"
private const val PLACEHOLDER = " "
private val CONFIG = LogConfig()
private var sExecutor: ExecutorService? = null

fun logGetConfig() = CONFIG

fun logV(vararg contents: Any) = log(V, CONFIG.mGlobalTag, *contents)

fun logVTag(tag: String, vararg contents: Any) = log(V, tag, *contents)

fun logD(vararg contents: Any) = log(D, CONFIG.mGlobalTag, *contents)

fun logDTag(tag: String, vararg contents: Any) = log(D, tag, *contents)

fun logI(vararg contents: Any) = log(I, CONFIG.mGlobalTag, *contents)

fun logITag(tag: String, vararg contents: Any) = log(I, tag, *contents)

fun logW(vararg contents: Any) = log(W, CONFIG.mGlobalTag, *contents)

fun logWTag(tag: String, vararg contents: Any) = log(W, tag, *contents)

fun logE(vararg contents: Any) = log(E, CONFIG.mGlobalTag, *contents)

fun logETag(tag: String, vararg contents: Any) = log(E, tag, *contents)

fun logA(vararg contents: Any) = log(A, CONFIG.mGlobalTag, *contents)

fun logATag(tag: String, vararg contents: Any) = log(A, tag, *contents)

fun logFile(content: Any) = log(FILE or D, CONFIG.mGlobalTag, content)

fun logFile(@TYPE type: Int, content: Any) = log(FILE or type, CONFIG.mGlobalTag, content)

fun logFile(tag: String, content: Any) = log(FILE or D, tag, content)

fun logFile(@TYPE type: Int, tag: String, content: Any) = log(FILE or type, tag, content)

fun logJson(content: String) = log(JSON or D, CONFIG.mGlobalTag, content)

fun logJson(@TYPE type: Int, content: String) = log(JSON or type, CONFIG.mGlobalTag, content)

fun logJson(tag: String, content: String) = log(JSON or D, tag, content)

fun logJson(@TYPE type: Int, tag: String, content: String) = log(JSON or type, tag, content)

fun logXml(content: String) = log(XML or D, CONFIG.mGlobalTag, content)

fun logXml(@TYPE type: Int, content: String) = log(XML or type, CONFIG.mGlobalTag, content)

fun logXml(tag: String, content: String) = log(XML or D, tag, content)

fun logXml(@TYPE type: Int, tag: String, content: String) = log(XML or type, tag, content)

fun log(type: Int, tag: String?, vararg contents: Any) {
//    if (!isDebug()) return
    if (!CONFIG.mLogSwitch || !CONFIG.mLog2ConsoleSwitch && !CONFIG.mLog2FileSwitch) return
    val typeLow = type and 0x0f
    val typeHigh = type and 0xf0
    if (typeLow < CONFIG.mConsoleFilter && typeLow < CONFIG.mFileFilter) return
    val tagHead = processTagAndHead(tag)
    val body = processBody(typeHigh, *contents)
    if (CONFIG.mLog2ConsoleSwitch && typeLow >= CONFIG.mConsoleFilter && typeHigh != FILE) {
        print2Console(typeLow, tagHead.tag, tagHead.consoleHead, body)
    }
    if ((CONFIG.mLog2FileSwitch || typeHigh == FILE) && typeLow >= CONFIG.mFileFilter) {
        print2File(typeLow, tagHead.tag, tagHead.fileHead + body)
    }
}

private fun processTagAndHead(tag: String?): TagHead {
    var tag1 = tag
    if (!CONFIG.mTagIsSpace && !CONFIG.mLogHeadSwitch) {
        tag1 = CONFIG.mGlobalTag
    } else {
        val stackTrace = Throwable().stackTrace
        val stackIndex = 3 + CONFIG.mStackOffset
        if (stackIndex >= stackTrace.size) {
            val targetElement = stackTrace[3]
            val fileName = getFileName(targetElement)
            if (CONFIG.mTagIsSpace && isSpace(tag1)) {
                val index = fileName.indexOf('.')// Use proguard may not find '.'.
                tag1 = if (index == -1) fileName else fileName.substring(0, index)
            }
            return TagHead(tag1 ?: "", null, ": ")
        }
        var targetElement = stackTrace[stackIndex]
        val fileName = getFileName(targetElement)
        if (CONFIG.mTagIsSpace && isSpace(tag1)) {
            val index = fileName.indexOf('.')// Use proguard may not find '.'.
            tag1 = if (index == -1) fileName else fileName.substring(0, index)
        }
        if (CONFIG.mLogHeadSwitch) {
            val tName = Thread.currentThread().name
            val head = Formatter()
                    .format("%s, %s.%s(%s:%d)",
                            tName,
                            targetElement.className,
                            targetElement.methodName,
                            fileName,
                            targetElement.lineNumber)
                    .toString()
            val fileHead = " [$head]: "
            if (CONFIG.mStackDeep <= 1) {
                return TagHead(tag1 ?: "", arrayOf(head), fileHead)
            } else {
                val consoleHead = arrayOfNulls<String>(Math.min(
                        CONFIG.mStackDeep,
                        stackTrace.size - stackIndex
                ))
                consoleHead[0] = head
                val spaceLen = tName.length + 2
                val space = Formatter().format("%" + spaceLen + "s", "").toString()
                var i = 1
                val len = consoleHead.size
                while (i < len) {
                    targetElement = stackTrace[i + stackIndex]
                    consoleHead[i] = Formatter()
                            .format("%s%s.%s(%s:%d)",
                                    space,
                                    targetElement.className,
                                    targetElement.methodName,
                                    getFileName(targetElement),
                                    targetElement.lineNumber)
                            .toString()
                    ++i
                }
                return TagHead(tag1 ?: "", consoleHead, fileHead)
            }
        }
    }
    return TagHead(tag1 ?: "", null, ": ")
}

private fun getFileName(targetElement: StackTraceElement): String {
    val fileName = targetElement.fileName
    if (fileName != null) return fileName
    // If name of file is null, should add
    // "-keepattributes SourceFile,LineNumberTable" in proguard file.
    var className = targetElement.className
    val classNameInfo = className.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
    if (classNameInfo.isNotEmpty()) {
        className = classNameInfo[classNameInfo.size - 1]
    }
    val index = className.indexOf('$')
    if (index != -1) {
        className = className.substring(0, index)
    }
    return "$className.java"
}

private fun processBody(type: Int, vararg contents: Any?): String {
    var body = NULL
    if (contents.size == 1) {
        val `object` = contents[0]
        if (`object` != null) body = `object`.toString()
        if (type == JSON) {
            body = formatJson(body)
        } else if (type == XML) {
            body = formatXml(body)
        }
    } else {
        val sb = StringBuilder()
        var i = 0
        val len = contents.size
        while (i < len) {
            val content = contents[i]
            sb.append(ARGS)
                    .append("[")
                    .append(i)
                    .append("]")
                    .append(" = ")
                    .append(content?.toString() ?: NULL)
                    .append(LINE_SEP)
            ++i
        }
        body = sb.toString()
    }
    return if (body.isEmpty()) NOTHING else body
}

private fun formatJson(json: String): String {
    var json1 = json
    try {
        if (json1.startsWith("{")) {
            json1 = JSONObject(json1).toString(4)
        } else if (json1.startsWith("[")) {
            json1 = JSONArray(json1).toString(4)
        }
    } catch (e: JSONException) {
        e.printStackTrace()
    }

    return json1
}

private fun formatXml(xml: String): String {
    var xml1 = xml
    try {
        val xmlInput = StreamSource(StringReader(xml1))
        val xmlOutput = StreamResult(StringWriter())
        val transformer = TransformerFactory.newInstance().newTransformer()
        transformer.setOutputProperty(OutputKeys.INDENT, "yes")
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
        transformer.transform(xmlInput, xmlOutput)
        xml1 = xmlOutput.writer.toString().replaceFirst(">".toRegex(), ">$LINE_SEP")
    } catch (e: Exception) {
        e.printStackTrace()
    }

    return xml1
}

@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
private fun print2Console(type: Int,
                          tag: String,
                          head: Array<String?>?,
                          msg: String) {
    if (CONFIG.mSingleTagSwitch) {
        val sb = StringBuilder()
        sb.append(PLACEHOLDER).append(LINE_SEP)
        if (CONFIG.mLogBorderSwitch) {
            sb.append(TOP_BORDER).append(LINE_SEP)
            if (head != null) {
                for (aHead in head) {
                    sb.append(LEFT_BORDER).append(aHead).append(LINE_SEP)
                }
                sb.append(MIDDLE_BORDER).append(LINE_SEP)
            }
            for (line in msg.split(LINE_SEP!!.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
                sb.append(LEFT_BORDER).append(line).append(LINE_SEP)
            }
            sb.append(BOTTOM_BORDER)
        } else {
            if (head != null) {
                for (aHead in head) {
                    sb.append(aHead).append(LINE_SEP)
                }
            }
            sb.append(msg)
        }
        printMsgSingleTag(type, tag, sb.toString())
    } else {
        printBorder(type, tag, true)
        printHead(type, tag, head)
        printMsg(type, tag, msg)
        printBorder(type, tag, false)
    }
}

private fun printBorder(type: Int, tag: String, isTop: Boolean) {
    if (CONFIG.mLogBorderSwitch) {
        Log.println(type, tag, if (isTop) TOP_BORDER else BOTTOM_BORDER)
    }
}

private fun printHead(type: Int, tag: String, head: Array<String?>?) {
    if (head != null) {
        for (aHead in head) {
            Log.println(type, tag, if (CONFIG.mLogBorderSwitch) LEFT_BORDER + aHead else aHead!!)
        }
        if (CONFIG.mLogBorderSwitch) Log.println(type, tag, MIDDLE_BORDER)
    }
}

private fun printMsg(type: Int, tag: String, msg: String) {
    val len = msg.length
    val countOfSub = len / MAX_LEN
    if (countOfSub > 0) {
        var index = 0
        for (i in 0 until countOfSub) {
            printSubMsg(type, tag, msg.substring(index, index + MAX_LEN))
            index += MAX_LEN
        }
        if (index != len) {
            printSubMsg(type, tag, msg.substring(index, len))
        }
    } else {
        printSubMsg(type, tag, msg)
    }
}

private fun printMsgSingleTag(type: Int, tag: String, msg: String) {
    val len = msg.length
    val countOfSub = len / MAX_LEN
    if (countOfSub > 0) {
        if (CONFIG.mLogBorderSwitch) {
            Log.println(type, tag, msg.substring(0, MAX_LEN) + LINE_SEP + BOTTOM_BORDER)
            var index = MAX_LEN
            for (i in 1 until countOfSub) {
                Log.println(type, tag, PLACEHOLDER + LINE_SEP + TOP_BORDER + LINE_SEP
                        + LEFT_BORDER + msg.substring(index, index + MAX_LEN)
                        + LINE_SEP + BOTTOM_BORDER)
                index += MAX_LEN
            }
            if (index != len) {
                Log.println(type, tag, PLACEHOLDER + LINE_SEP + TOP_BORDER + LINE_SEP
                        + LEFT_BORDER + msg.substring(index, len))
            }
        } else {
            var index = 0
            for (i in 0 until countOfSub) {
                Log.println(type, tag, msg.substring(index, index + MAX_LEN))
                index += MAX_LEN
            }
            if (index != len) {
                Log.println(type, tag, msg.substring(index, len))
            }
        }
    } else {
        Log.println(type, tag, msg)
    }
}

@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
private fun printSubMsg(type: Int, tag: String, msg: String) {
    if (!CONFIG.mLogBorderSwitch) {
        Log.println(type, tag, msg)
        return
    }
    val lines = msg.split(LINE_SEP!!.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
    for (line in lines) {
        Log.println(type, tag, LEFT_BORDER + line)
    }
}

@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
private fun printSubMsg1(type: Int, tag: String, msg: String) {
    if (!CONFIG.mLogBorderSwitch) {

        return
    }
    val lines = msg.split(LINE_SEP!!.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
    for (line in lines) {
        Log.println(type, tag, LEFT_BORDER + line)
    }
}

private fun print2File(type: Int, tag: String, msg: String) {
    val now = Date(System.currentTimeMillis())
    val format = FORMAT.format(now)
    val date = format.substring(0, 5)
    val time = format.substring(6)
    val fullPath = ((if (CONFIG.mDir == null) CONFIG.mDefaultDir else CONFIG.mDir)
            + CONFIG.mFilePrefix + "-" + date + ".txt")
    if (!File(fullPath).createOrExistsFile()) {
        Log.e("LogUtils", "create $fullPath failed!")
        return
    }
    val sb = StringBuilder()
    sb.append(time)
            .append(logT[type - V])
            .append("/")
            .append(tag)
            .append(msg)
            .append(LINE_SEP)
    val content = sb.toString()
    input2File(content, fullPath)
}

@Suppress("DEPRECATION")
private fun printDeviceInfo(filePath: String) {
    var versionName = ""
    var versionCode = 0
    try {
        val pi = getApp()
                .packageManager
                .getPackageInfo(getApp().packageName, 0)
        if (pi != null) {
            versionName = pi.versionName
            versionCode = pi.versionCode
        }
    } catch (e: PackageManager.NameNotFoundException) {
        e.printStackTrace()
    }

    val time = filePath.substring(filePath.length - 9, filePath.length - 4)
    val head = "************* Log Head ****************" +
            "\nDate of Log        : " + time +
            "\nDevice Manufacturer: " + Build.MANUFACTURER +
            "\nDevice Model       : " + Build.MODEL +
            "\nAndroid Version    : " + Build.VERSION.RELEASE +
            "\nAndroid SDK        : " + Build.VERSION.SDK_INT +
            "\nApp VersionName    : " + versionName +
            "\nApp VersionCode    : " + versionCode +
            "\n************* Log Head ****************\n\n"
    input2File(head, filePath)
}

private fun input2File(input: String, filePath: String) {
    if (sExecutor == null) {
        sExecutor = Executors.newSingleThreadExecutor()
    }
    val submit = sExecutor!!.submit(Callable {
        var bw: BufferedWriter? = null
        try {
            bw = BufferedWriter(FileWriter(filePath, true))
            bw.write(input)
            return@Callable true
        } catch (e: IOException) {
            e.printStackTrace()
            return@Callable false
        } finally {
            try {
                bw?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }

        }
    })
    try {
        if (submit.get()) return
    } catch (e: InterruptedException) {
        e.printStackTrace()
    } catch (e: ExecutionException) {
        e.printStackTrace()
    }

    Log.e("LogUtils", "log to $filePath failed!")
}

open class LogConfig {

    var mDefaultDir: String? = null// The default storage directory of log.
    var mDir: String? = null       // The storage directory of log.
    var mFilePrefix = "util"// The file prefix of log.
    var mLogSwitch = true  // The switch of log.
    var mLog2ConsoleSwitch = true  // The logcat's switch of log.
    var mGlobalTag: String? = null  // The global tag of log.
    var mTagIsSpace = true  // The global tag is space.
    var mLogHeadSwitch = true  // The head's switch of log.
    var mLog2FileSwitch = false // The file's switch of log.
    var mLogBorderSwitch = true  // The border's switch of log.
    var mSingleTagSwitch = true  // The single tag of log.
    var mConsoleFilter = V     // The console's filter of log.
    var mFileFilter = V     // The file's filter of log.
    var mStackDeep = 1     // The stack's deep of log.
    var mStackOffset = 0     // The stack's offset of log.

    @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
    constructor() {
        if (mDefaultDir != null) return
        mDefaultDir = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() && getApp().externalCacheDir != null)
            getApp().externalCacheDir.toString() + FILE_SEP + "log" + FILE_SEP
        else {
            getApp().cacheDir.toString() + FILE_SEP + "log" + FILE_SEP
        }
    }

    fun setLogSwitch(logSwitch: Boolean): LogConfig {
        mLogSwitch = logSwitch
        return this
    }

    fun setConsoleSwitch(consoleSwitch: Boolean): LogConfig {
        mLog2ConsoleSwitch = consoleSwitch
        return this
    }

    fun setGlobalTag(tag: String): LogConfig {
        if (isSpace(tag)) {
            mGlobalTag = ""
            mTagIsSpace = true
        } else {
            mGlobalTag = tag
            mTagIsSpace = false
        }
        return this
    }

    fun setLogHeadSwitch(logHeadSwitch: Boolean): LogConfig {
        mLogHeadSwitch = logHeadSwitch
        return this
    }

    fun setLog2FileSwitch(log2FileSwitch: Boolean): LogConfig {
        mLog2FileSwitch = log2FileSwitch
        return this
    }

    fun setDir(dir: String): LogConfig {
        if (isSpace(dir)) {
            mDir = null
        } else {
            mDir = if (dir.endsWith(FILE_SEP!!)) dir else dir + FILE_SEP
        }
        return this
    }

    fun setDir(dir: File?): LogConfig {
        mDir = if (dir == null) null else dir.absolutePath + FILE_SEP
        return this
    }

    fun setFilePrefix(filePrefix: String): LogConfig {
        if (isSpace(filePrefix)) {
            mFilePrefix = "util"
        } else {
            mFilePrefix = filePrefix
        }
        return this
    }

    fun setBorderSwitch(borderSwitch: Boolean): LogConfig {
        mLogBorderSwitch = borderSwitch
        return this
    }

    fun setSingleTagSwitch(singleTagSwitch: Boolean): LogConfig {
        mSingleTagSwitch = singleTagSwitch
        return this
    }

    fun setConsoleFilter(@TYPE consoleFilter: Int): LogConfig {
        mConsoleFilter = consoleFilter
        return this
    }

    fun setFileFilter(@TYPE fileFilter: Int): LogConfig {
        mFileFilter = fileFilter
        return this
    }

    fun setStackDeep(@IntRange(from = 1) stackDeep: Int): LogConfig {
        mStackDeep = stackDeep
        return this
    }

    fun setStackOffset(@IntRange(from = 0) stackOffset: Int): LogConfig {
        mStackOffset = stackOffset
        return this
    }

    override fun toString(): String {
        return ("switch: " + mLogSwitch
                + LINE_SEP + "console: " + mLog2ConsoleSwitch
                + LINE_SEP + "tag: " + (if (mTagIsSpace) "null" else mGlobalTag)
                + LINE_SEP + "head: " + mLogHeadSwitch
                + LINE_SEP + "file: " + mLog2FileSwitch
                + LINE_SEP + "dir: " + (if (mDir == null) mDefaultDir else mDir)
                + LINE_SEP + "filePrefix: " + mFilePrefix
                + LINE_SEP + "border: " + mLogBorderSwitch
                + LINE_SEP + "singleTag: " + mSingleTagSwitch
                + LINE_SEP + "consoleFilter: " + logT[mConsoleFilter - V]
                + LINE_SEP + "fileFilter: " + logT[mFileFilter - V]
                + LINE_SEP + "stackDeep: " + mStackDeep
                + LINE_SEP + "mStackOffset: " + mStackOffset)
    }


}

private class TagHead internal constructor(internal var tag: String, internal var consoleHead: Array<String?>?, internal var fileHead: String)